﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Affinity.Synthesis.ExpressionCells.Basis;
using Affinity.Core.Utility;

namespace Affinity.Synthesis.ExpressionCells
{
    /// <summary>
    /// Converts expression trees into expression cell trees.
    /// </summary>
    internal class ExpressionCellularizer
    {
        /// <summary>
        /// Returns an expression cell tree for the given expresssion tree.
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public IExpressionCell Cellularize(Expression expression)
        {
            LambdaExpression lambdaExpression = expression as LambdaExpression;
            if (lambdaExpression == null) return _Visitor.Cellularize(expression);
            return _Visitor.Cellularize(lambdaExpression.Body, lambdaExpression.Parameters.ToArray());
        }

        private Visitor _Visitor = new Visitor();

        /// <summary>
        /// The visitor class that converts expression trees into expression cell trees.
        /// </summary>
        private class Visitor : ExpressionVisitor
        {
            /// <summary>
            /// Returns an expression cell tree corresponding to the given expression tree and expression tree's parameters.
            /// </summary>
            /// <param name="expression"></param>
            /// <param name="parameters"></param>
            /// <returns></returns>
            public IExpressionCell Cellularize(Expression expression, params ParameterExpression[] parameters)
            {
                PopulateParameters(parameters);
                List<IExpressionCell> topLevelInputCells = new List<IExpressionCell>();
                _AnalysisStack.Push(topLevelInputCells);
                Visit(expression);
                _AnalysisStack.Pop();

                IExpressionCell finalCell = topLevelInputCells[0];
#if DEBUG
                //TreeGraph treeGraph = TreeGraph.ConstructFromFractal(finalCell, parentCell => parentCell.Inputs.Where(input => input != null).Select(input => input.OwningExpressionCell));
                //Debug.WriteLine("\r\n=====================\r\nCreated expression cell tree:\r\n\r\n{0}\r\n=====================\r\n", GraphTextProjector.ProjectGraph(treeGraph));
#endif
                return new CompositeExpressionCell(finalCell, _Parameters.Values.ToArray()); ;
            }

            /// <summary>
            /// Analyzes an expression (sub)tree and adds an expression cell (sub)tree corresponding to it.
            /// </summary>
            /// <param name="node"></param>
            /// <returns></returns>
            public override Expression Visit(Expression node)
            {
                List<IExpressionCell> inputCells = new List<IExpressionCell>();

                _AnalysisStack.Push(inputCells);
                node = base.Visit(node);
                _AnalysisStack.Pop();

                if (node != null)
                {
                    IExpressionCell expressionCell = CreateExpressionCell(node, inputCells);
                    //if (expressionCell == null) throw new Exception("Expression cell may not be null.");
                    if (expressionCell != null) _AnalysisStack.Peek().Add(expressionCell);
                }

                return node;
            }

            /// <summary>
            /// Visits the parameters of the given lambda expression but otherwise returns it unexamined and unchanged.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="node"></param>
            /// <returns></returns>
            protected override Expression VisitLambda<T>(Expression<T> node)
            {
                // Don't examine nested lambdas.
                foreach (ParameterExpression parameterExpression in _Parameters.Keys) Visit(parameterExpression);
                return node;
            }

            /// <summary>
            /// Creates an expression cell for the given expression and input expression cells, after isolating ("atomizing")
            /// it if needed.
            /// </summary>
            /// <param name="node"></param>
            /// <param name="inputCells"></param>
            /// <returns></returns>
            private IExpressionCell CreateExpressionCell(Expression node, IEnumerable<IExpressionCell> inputCells)
            {
                IExpressionCell expressionCell = null;

                switch (node.NodeType)
                {
                    case ExpressionType.Call: expressionCell = CreateExpressionCell((MethodCallExpression)node); break;
                    case ExpressionType.MemberAccess: expressionCell = CreateExpressionCell((MemberExpression)node); break;
                    case ExpressionType.Parameter: expressionCell = GetParameterExpressionCell((ParameterExpression)node); break;
                    case ExpressionType.Lambda: expressionCell = new LambdaExpressionCell((LambdaExpression)node, _Parameters.Keys.ToArray()); break;

                    default: expressionCell = GenericExpressionCell.Create(_Atomizer.Atomize(node)); break;
                }

                if (expressionCell != null) expressionCell.Inputs = inputCells.Select(inputCell => inputCell.Output).ToArray();

                return expressionCell;
            }

            /// <summary>
            /// Gets the parameter expression cell corresponding to the given parameter expression, or null if none has been created for it.
            /// </summary>
            /// <param name="node"></param>
            /// <returns></returns>
            private IExpressionCell GetParameterExpressionCell(ParameterExpression node)
            {
                // Allow for unknown parameters, for nested lambdas.

                ParameterExpressionCell expressionCell = null;
                _Parameters.TryGetValue(node, out expressionCell);
                return expressionCell;
            }

            /// <summary>
            /// Creates an expression cell for the given MethodCallExpression after isolating ("atomizing") it.
            /// </summary>
            /// <param name="node"></param>
            /// <returns></returns>
            private IExpressionCell CreateExpressionCell(MethodCallExpression node)
            {                
                return MethodInvocationCell.Create(node, Expressions.CompileSync(_Atomizer.Atomize(node)));
            }

            /// <summary>
            /// Creates an expression cell for the given MemberExpression after isolating ("atomizing") it.
            /// </summary>
            /// <param name="node"></param>
            /// <returns></returns>
            private IExpressionCell CreateExpressionCell(MemberExpression node)
            {
                if (node.Member is PropertyInfo) return new PropertyAccessCell((PropertyInfo)node.Member, Expressions.CompileSync(_Atomizer.Atomize(node)), node);
                else return GenericExpressionCell.Create(_Atomizer.Atomize(node));
            }

            /// <summary>
            /// Creates and registers ParameterExpressionCells corresponding to the given ParameterExpressions.
            /// </summary>
            /// <param name="parameterExpressions"></param>
            private void PopulateParameters(ParameterExpression[] parameterExpressions)
            {
                int ordinal = 1;

                foreach (ParameterExpression parameter in parameterExpressions)
                {
                    _Parameters[parameter] = new ParameterExpressionCell(ordinal++, parameter);
                }
            }

            private Stack<List<IExpressionCell>> _AnalysisStack = new Stack<List<IExpressionCell>>();
            private ExpressionAtomizer _Atomizer = new ExpressionAtomizer();
            private Dictionary<ParameterExpression, ParameterExpressionCell> _Parameters = new Dictionary<ParameterExpression, ParameterExpressionCell>();
        }
    }
}
